원본 항목: Part 5. 코드 오류 — 9. 무한 자원 할당CWE-770repo 폴더: sd51_unbounded/ (빌드·실행은 §3-1)
목표 시간: 1회차 10분 / 2회차 6분 / 3회차 4분
Part 5(코드 오류)의 마지막 드릴이다.
실제 accept()/pthread_create() 대신, “지금 활성 연결이 몇 개인가”를 정수 하나로 추적해 상한의 유무가 결과에 미치는 영향을 요청 10개로 직접 관찰한다.
이 드릴의 문서 구성 (SD01~SD58 공통)
① 요구사항서 — 어떤 코드가 뚫려 있고, 무엇을 만드나.
② pseudo code 템플릿 — Bad 와 Good 의 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 공격은 막히고 정상은 통과해야 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 같은 치환 규칙을 다른 소재로 다시 치는 것이다.
0. 이 드릴로 체화할 것
int handle_connection(void) {- g_active++; /* 접속마다 상한 없이 생성 */- return 0;+ if (g_active >= POOL_MAX) return -1; /* Pool 초과 시 거부 */+ g_active++;+ return 0; }
“요청당 무제한 할당”은 공격자에게 준 무료 DoS 버튼이다.
메시지 길이든 연결 수든 스레드 수든, 요청 하나가 만들 수 있는 자원에는 항상 상한이 있어야 한다.
회차별 진행
회차
변형
보는 것
순서
1회차
A 동시 접속 수 제한
① 요구사항서 ② pseudo code 전체
1. Bad 를 그대로 쳐서 취약점을 관찰한다. 2. Good pseudo 를 한 줄씩 코드로 바꾼다. 3. ③ 테스트를 실행한다.
2회차
B 메시지 길이 상한으로 변형
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. Bad 를 치고 취약점을 확인한다. 2. 번호만 보고 Good 을 쓴다. 3. ③ 테스트를 실행한다.
3회차
C 연결 반환(release)까지 포함한 전체 수명주기로 확장
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 Bad 부터 Good 까지 재현한다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
1. 요구사항서
1-A. 변형 A — 동시 접속 수 제한
서버는 동시에 여러 클라이언트의 접속을 받아들인다.
동시 활성 연결 수에는 상한이 있어야 한다.
상한을 넘는 요청은 거부해야 한다(서비스 자체는 계속 동작해야 한다).
함수
int active_count(void);void reset_active(void);int handle_connection_bad(void);int handle_connection_good(void);
신뢰 경계
값
출처
검증 없이 흘러가는 곳
연결 요청 횟수
외부(클라이언트)
요청마다 무조건 자원을 늘리는 카운터 증가 지점
공격 입력표
상황
Bad 결과
이유
연결 요청 10회
활성 연결 10개 전부 생성(상한 없음)
요청 횟수만큼 그대로 자원이 늘어난다 — 공격자가 소수의 요청만으로도 자원을 무한정 소진시킬 수 있다
같은 상황(Good)
POOL_MAX(4)개까지만 수락, 나머지 6개는 거부
상한에 도달하면 이후 요청은 실패로 응답하고 자원을 늘리지 않는다
1-B. 변형 B — 메시지 길이 상한으로 변형
가이드 원문의 또 다른 축인 “요청 메시지 길이”에 상한을 두는 함수로 바꿔본다.
unsigned 길이 인자에 MAX_MSG_LEN 상수 검사를 추가하는 형태
1-C. 변형 C — 연결 반환(release)까지 포함한 전체 수명주기로 확장
release_connection()을 추가해, 상한에 걸려 거부됐던 요청도 기존 연결이 반환되면 다시 수락될 수 있게 만든다 — 단순 거부가 아니라 진짜 “풀(pool)“처럼 재사용되는 구조를 완성한다
2. pseudo code 템플릿 — 보고 쳐서 익힌다
사용법
변형별로 아래 pseudo code 를 소스 파일에 주석만 복사한다.
코드는 주석을 보고 내가 쓴다.
다 쓰면 [ ] 를 x 로 바꾼다.
막히면 §5 모범답안(A)의 해당 단계만 본다.
2-A. handle_connection_good — Level 1 (1회차, 전부 본다)
먼저 Bad 를 그대로 쳐서 §3 의 test_bad_is_vulnerable 이 통과하는지(= 실제로 뚫리는지) 확인한다.
그다음 Good 을 쓴다.
int handle_connection_good(void){ /* 아무것도 안 본다. * §1 요구사항서와 §0 의 치환 규칙만 보고 친다. * * 막히면 아래만 찾아본다. * - `>=` 와 `>` 의 차이 * - `reset_active()` 로 시험 사이에 상태 초기화 */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
공격 입력표(§1)와 테스트가 답이다.
#include "unbounded.h"static int g_active = 0;int active_count(void) { return g_active; }void reset_active(void) { g_active = 0; }int handle_connection_bad(void){ g_active++; /* 접속마다 상한 없이 생성 */ return 0;}int handle_connection_good(void){ if (g_active >= POOL_MAX) return -1; /* Pool 초과 시 거부 */ g_active++; return 0;}
눈여겨볼 점: handle_connection_good은 handle_connection_bad보다 딱 한 줄(if (g_active >= POOL_MAX) return -1;) 앞서 나온다.
Part 5(코드 오류) 9개 항목 중 상당수가 이렇게 “몇 줄 안 되는 차이”로 갈렸다는 걸 되짚어보면, 코드 오류 계열의 본질은 복잡한 로직이 아니라 빠뜨리기 쉬운 한두 줄에 있다는 걸 알 수 있다.